/* 
	@Copyright (c) 2007 by Denis Riabtchik. All rights reserved. See license.txt and http://jgrouse.com for details@
	$Id: DocManager.java 596 2013-04-13 20:38:09Z denis.riabtchik@gmail.com $
*/
package jgrouse.jgdoc.elements;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import jgrouse.jgdoc.JGDocException;
import jgrouse.jgdoc.TokenType;
import jgrouse.jgdoc.Vocabulary;
import jgrouse.jgdoc.api.IComment;
import jgrouse.jgdoc.api.IContainerElement;
import jgrouse.jgdoc.api.IDocElement;
import jgrouse.jgdoc.api.IDocElementList;
import jgrouse.jgdoc.api.IDocManager;
import jgrouse.jgdoc.api.ILink;
import jgrouse.jgdoc.api.IRefContainer;




public class DocManager implements IDocManager, Serializable
{
	private static final long serialVersionUID = -8574467725797200906L;
	
	public static final String GLOBAL_NAMESPACE = "GLOBAL";
	
	private FileElement m_currentFile;
	private RefContainer m_currentModule;
	private IContainerElement m_currentNamespace;
	private String m_encoding;
	private IDocElement m_project;
	
	private Map<String, IDocElement> m_allComments;
	private transient boolean m_stopOnErrors;
	private transient boolean m_suppressGlobalWarnings = false;
	
	private transient List<String> m_messages = new LinkedList<String>();
	private Map<String, String> m_properties;
	private String m_srcUrl;
	
	private transient IDocElementList m_modules;
	private transient IDocElementList m_files;
	private transient IDocElementList m_logical;
	
	private boolean m_suppressPrivates;
	
	public DocManager()
	{
		m_allComments = new TreeMap<String, IDocElement>();
	}
	
	public void setProperties(Map<String, String> props)
	{
		m_properties = props;
	}
	
	public Map<String, String> getProperties()
	{
		return m_properties;
	}
	
	public void handleException(JGDocException ex) throws JGDocException
	{
		if (m_stopOnErrors)
		{
			throw ex;
		}
		else
		{
      warn(ex.getFile(), ex.getLine(), ex.getMessage());
		}
	}
	
	public String toString()
	{
		StringBuffer buf = new StringBuffer("<?xml version=\"1.0\" encoding=\"")
			.append(getEncoding()).append("\" ?>\n");
		
		buf.append("<jgdoc>");
		if (getProperties().size() > 0)
		{
			buf.append("<properties>");
			for (Map.Entry<String, String> entry : getProperties().entrySet())
			{
				buf.append("<property name=\"").append(entry.getKey()).
					append("\" value=\"").append(entry.getValue()).append("\"/>");
			}
			buf.append("</properties>");
		}
		if (getProject() != null)
		{
			buf.append(getProject().toString());
		}
		buf.append("<items>");
		for (IDocElement de : all())
		{
			buf.append(de.toString());
		}
		buf.append("</items>");
		buf.append("</jgdoc>");
		return buf.toString();
		
	}
	
	
	private void resolveLink(IDocElement de, ILink pLink) throws JGDocException
	{
		String ref = pLink.getLink().trim();
		if (ILink.Util.isExternalLink(ref)) {
			pLink.setResolvedPath(ref);
			pLink.setExternal(true);
			return;
		}
		if (ref.length() > 2 && "[]".equals(ref.substring(ref.length() - 2)))
		{
			ref = ref.substring(0, ref.length() - 2).trim();
		}
		IDocElement referenced = null;
		if (ref.indexOf('.') < 0)
		{
			String parent = de.getOriginalParentName();
			if (parent != null)
			{
				String newName = parent + "." + ref;
				referenced = m_allComments.get(newName);
				if (referenced == null)
				{
					newName = DocManager.GLOBAL_NAMESPACE + "." + ref;
					referenced = m_allComments.get(newName);
				}
			}
			if (referenced == null)
			{
				List<IDocElement> found = new ArrayList<IDocElement>();
				for (IDocElement e : m_allComments.values())
				{
					if (e.getLocalName().equals(ref))
					{
						found.add(e);
						referenced = e;
					}
				}
				if (found.size() != 1)
				{
					referenced = null;
				}
			}
		}
		if (referenced == null)
		{
			referenced = m_allComments.get(ref);
		}
		if (referenced != null)
		{
			if (m_suppressPrivates)
			{
				if (referenced.getComment().hasModifer("private"))
				{
					throw new JGDocException("Referencing private member " + pLink.getText(), pLink.getComment().getFile(), pLink.getComment().getLine());
				}
			}
			pLink.setResolvedPath(referenced.getFullName());
			if (referenced.getComment() != null && referenced.getComment().getCommentType().equals(Vocabulary.MODULE))
			{
				pLink.setText(referenced.getComment().getName());
			}
		}
	}
	
	public void postProcess() throws JGDocException 
	{
		for (Map.Entry<String, IDocElement> entry : m_allComments.entrySet())
		{
			List<ILink> l = new ArrayList<ILink>();
			IDocElement de = entry.getValue();
			if (de.getComment() != null)
			{
				de.getComment().collectLinks(l);
				for (ILink link : l)
				{
					resolveLink(de, link);
				}
			}
			entry.getValue().setupReferences(m_allComments);
		}
		
		
		if (m_suppressPrivates)
		{
			Iterator<Map.Entry<String, IDocElement>> iter = m_allComments.entrySet().iterator();
			while (iter.hasNext())
			{
				Map.Entry<String, IDocElement> entry = iter.next();
				if (entry.getValue().getComment() != null)
				{
					if (entry.getValue().getComment().hasModifer("private"))
					{
						iter.remove();
						continue;
					}
					entry.getValue().getComment().cleanupPrivates();
				}
			}
			
			for (IDocElement de : m_allComments.values())
			{
				if (de instanceof IRefContainer)
				{
					IRefContainer rc = (IRefContainer) de;
					rc.cleanupPrivates(this);
				}
			}
			
		}
		
		for (IDocElement de : m_allComments.values())
		{
			try
			{
				de.postProcess();
			}
			catch (JGDocException ex)
			{
				handleException(ex);
			}
		}		
		
		
		if (m_project == null)
		{
			m_project = new DocElement();
			Comment c = new Comment("", 0);
			CommentTag ct = new CommentTag(Vocabulary.PROJECT, new Content(), null, null, null, c);
			c.addCommentTag(ct);
			m_project.setComment(c);
		}
		m_project.setTimestamp(new Date().toString());
	}
	
	public void startFile(String fileName) throws JGDocException
	{
		FileElement file = (FileElement) m_allComments.get(fileName);
		if (file == null)
		{
			file = new FileElement();
			Comment fileComment = new Comment(fileName, 0);
			fileComment.addCommentTag(new CommentTag(Vocabulary.FILE,
					new Content().addContentSection(new ContentSection(fileName)), null, fileName, null, fileComment));
			file.setComment(fileComment);
			file.setFullName(fileName);
			m_allComments.put(file.getFullName(), file);
		}
		m_currentNamespace = null;
		m_currentModule = null;
		m_currentFile = file;
	}
	
	private void processFile(IComment pFileComment)
	{
		FileElement file = (FileElement) m_allComments.get(pFileComment.getName());
		if (file == null)
		{
			file = new FileElement();
			file.setFullName(pFileComment.getName());
			m_allComments.put(file.getFullName(), file);
		}
		file.setComment(pFileComment);
		m_currentNamespace = null;
		m_currentModule = null;
		m_currentFile = file;
	}
	
	private void processModule(IComment pModuleComment)
	{
		RefContainer module = new ModuleElement();
		module.setComment(pModuleComment);
		module.setFullName(pModuleComment.getName());
		m_allComments.put(module.getFullName(), module);
		m_currentFile.addChild(module);
		m_currentModule = module;
		m_currentNamespace = null;
	}
	
	private void processPhysicalComment(IComment pComment)
	{
		if (pComment.getCommentType() == Vocabulary.FILE)
		{
			processFile(pComment);
		}
		else
		{
			processModule(pComment);
		}
	}
	
	
	private void processLogicalComment(IComment pComment) throws JGDocException
	{
		String name = pComment.getName();
		m_currentNamespace = null;
		if (name.length() == 0)
		{
			name = DocManager.GLOBAL_NAMESPACE;
		}
		IContainerElement ce = getNamespace(name, pComment);
		if (ce.getComment() != null)
		{
			throw new JGDocException(name + " is already defined", pComment.getFile(), pComment.getLine());
		}
		ce.setComment(pComment);
		setNames(ce, pComment);
		if (pComment.getCommentType() != Vocabulary.NAMESPACE)
		{
			IRefContainer physOwner = m_currentModule == null? m_currentFile : m_currentModule;
			physOwner.addChild(ce);
		}
		IContainerElement parent = getNamespace(ce.getOriginalParentName(), pComment);
		parent.addChild(ce);
		ce.setOwner(parent);
		
		m_currentNamespace = ce;
	}
	
	
	private IContainerElement getNamespace(String parent, IComment pCurrentComment) throws JGDocException
	{
		if (parent.length() == 0)
		{
			parent = DocManager.GLOBAL_NAMESPACE;
		}
		IDocElement found = m_allComments.get(parent);
		if (found != null && !(found instanceof IContainerElement))
		{
		    throw new JGDocException(parent + " already defined as " + found.getComment().getCommentType() + " in " + found.getComment().getFile() + ":" + found.getComment().getLine(), 
		            pCurrentComment.getFile(), pCurrentComment.getLine());
		}
		IContainerElement ce = (IContainerElement) found;
		if (ce == null)
		{
			String[] s = parent.split("\\.");
			IContainerElement pce = null;
			if (!DocManager.GLOBAL_NAMESPACE.equals(parent))
			{
				pce = getNamespace(getParentName(s), pCurrentComment);
			}
			ce = new ContainerElement();
			ce.setFullName(parent);
			ce.setLocalName(s[s.length - 1]);
			m_allComments.put(ce.getFullName(), ce);
			if (pce != null)
			{
				pce.addChild(ce);
				ce.setOwner(pce);
				ce.setParentName(pce.getFullName());
			}
				
		}
		return ce;
		
	}
	
	public static String getParentName(String[] name)
	{
		List<String> l = new ArrayList<String>(Arrays.asList(name));
		l.remove(l.size() - 1);
		return DocElement.join(l, ".");
	}
	
	private void setNames(IDocElement de, IComment pComment) throws JGDocException
	{
		String name = de.getComment().getName();
		if (name == null)
		{
			throw new JGDocException("Name must be specified", pComment.getFile(), pComment.getLine());
		}
		String shortName = null;
		String fullName = null;
		String parentName = null;
		if (name.indexOf('.') >= 0)
		{
			fullName = name;
			parentName = getValidParent(name);
			if (parentName.length() == 0)
			{
				shortName = fullName;
			}
			else
			{
				shortName = fullName.substring(parentName.length() + 1);
			}
		}
		else
		{
			shortName = name;
			if (m_currentNamespace != null)
			{
				parentName = m_currentNamespace.getFullName();
				fullName = parentName + "." + name;
			}
			else
			{
				parentName = "";
				fullName = name;
			}
		}
		de.setFullName(fullName);
		de.setLocalName(shortName);
		de.setParentName(parentName);
	}
	
	private void issueGlobalWarning(IComment pComment)
	{
		if (!m_suppressGlobalWarnings)
		{
			warn(pComment.getFile(), pComment.getLine(), "No namespace declared for \"" + pComment.getName() + "\" (using GLOBAL)");
		}
	}
	
	private String getValidParent(String name)
	{
		String[] parts = name.split("\\.");
		if (parts.length == 1)
		{
			return "";
		}
		String parent = getParentName(parts);
		IDocElement candidate = m_allComments.get(parent);
		if (candidate == null || candidate instanceof IContainerElement)
		{
			return parent;
		}
		return getValidParent(parent);
		
	}
	
	private void processMember(IComment pComment) throws JGDocException
	{
		DocElement de = new DocElement();
		de.setComment(pComment);
		setNames(de, pComment);
		IDocElement found = m_allComments.get(de.getFullName());
		if (found != null)
		{
		    throw new JGDocException(de.getFullName() + " already defined as " + found.getComment().getCommentType() + " at " +
		            found.getComment().getFile() + ":" + found.getComment().getLine(), pComment.getFile(), pComment.getLine());
		}
		m_allComments.put(de.getFullName(), de);
		IRefContainer physOwner = m_currentModule == null? m_currentFile : m_currentModule;
		physOwner.addChild(de);
		if (de.getOriginalParentName() != null && de.getOriginalParentName().length() == 0)
		{
			issueGlobalWarning(pComment);
		}
		IContainerElement parent = getNamespace(de.getOriginalParentName(), pComment);
		parent.addChild(de);
		de.setOwner(parent);
	}
	
	private void processControl(IComment pComment) throws JGDocException
	{
		if (pComment.getCommentType() == Vocabulary.END)
		{
			if (m_currentNamespace != null)
			{
				m_currentNamespace = null;
			}
			else if (m_currentModule != null)
			{
				m_currentModule = null;
			}
			else
			{
				throw new JGDocException("Unexpected END statement", pComment.getFile(), pComment.getLine());
			}
		}
		else 
		{
			IContainerElement ce = getNamespace(pComment.getName(), pComment);
			m_currentNamespace = ce;
		}
			
	}
	
	private void processProject(IComment pComment) throws JGDocException
	{
		if (m_project != null)
		{
			throw new JGDocException("Project tag already defined ", pComment.getFile(), pComment.getLine());
		}
		m_project = new DocElement();
		m_project.setComment(pComment);
	}
	
	public void addComment(IComment pComment) throws JGDocException
	{
		TokenType tokenType = pComment.getTokenType();
		if (tokenType == TokenType.PHYS_CONTAINER)
		{
			processPhysicalComment(pComment);
		}
		else if (tokenType == TokenType.LOGICAL_CONTAINER)
		{
			processLogicalComment(pComment);
		}
		else if (tokenType == TokenType.FUNCTION || tokenType == TokenType.VARIABLE)
		{
			processMember(pComment);
		}
		else if (tokenType == TokenType.CONTROL)
		{
			processControl(pComment);
		}
		else if (tokenType == TokenType.PROJECT)
		{
			processProject(pComment);
		}
	}

	public void setEncoding(String pEncoding)
	{
		m_encoding = pEncoding;
	}
	
	public String getEncoding()
	{
		return m_encoding;
	}

	public void warn(String file, int line_no, String msg)
	{
		m_messages.add("Warning " + file + ":" + line_no + ": " + msg);
	}

	public List<String> getMessages()
	{
		return m_messages;
	}

	public boolean isStopOnErrors()
	{
		return m_stopOnErrors;
	}

	public void setStopOnErrors(boolean pStopOnErrors)
	{
		m_stopOnErrors = pStopOnErrors;
	}

	public boolean isSuppressGlobalWarnings()
	{
		return m_suppressGlobalWarnings;
	}

	public void setSuppressGlobalWarnings(boolean pSuppressGlobalWarnings)
	{
		m_suppressGlobalWarnings = pSuppressGlobalWarnings;
	}

	public String getSrcUrl()
	{
		return m_srcUrl;
	}

	public void setSrcUrl(String pSrcUrl)
	{
		m_srcUrl = pSrcUrl;
	}

	public Map<String, IDocElement> getAllComments()
	{
		return m_allComments;
	}

	public IDocElement getProject()
	{
		return m_project;
	}

	public IDocElement get(String pElementName)
	{
		return pElementName != null ? m_allComments.get(pElementName) : null;
	}

	public IDocElementList all()
	{
		IDocElementList result = new DocElementList();
		result.addAll(m_allComments.values());
		return result;
	}
	
	private IDocElementList getElementsOfType(Vocabulary voc)
	{
		IDocElementList result = new DocElementList();
		for (IDocElement de : all())
		{
			if (de.getComment().getCommentType() == voc)
			{
				result.add(de);
			}
		}
		return result;
	}
	
	public IDocElementList getModules()
	{
		if (m_modules != null)
		{
			return m_modules;
		}
		m_modules = getElementsOfType(Vocabulary.MODULE);
		return m_modules;
	}
	
	public IDocElementList getFiles()
	{
		if (m_files != null)
		{
			return m_files;
		}
		m_files = getElementsOfType(Vocabulary.FILE);
		return m_files;		
	}
	
	public IDocElementList getLogical()
	{
		if (m_logical != null)
		{
			return m_logical;
		}
		m_logical = new DocElementList();
		for (IDocElement de : m_allComments.values())
		{
			if (de.getComment().getCommentType().getTokenType() == TokenType.LOGICAL_CONTAINER)
			{
				m_logical.add(de);
			}
		}
		return m_logical;
	}

	public boolean getSuppressPrivates()
	{
		return m_suppressPrivates;
	}

	public void setSuppressPrivates(boolean pSuppressPrivates)
	{
		m_suppressPrivates = pSuppressPrivates;
	}

}
